﻿using System.Linq;
using System.Text;
using Ast.Core.WinApi.DbgHelp;
using Ast.Framework;
using Ast.Framework.Caching;
using Castle.Core.Logging;

namespace Ast.Core.Services
{
    public interface IStructureConverter : IDependency
    {
        string Convert(ulong baseOfDll, string symbol);
    }

    public class StructureConverter : IStructureConverter
    {
        private readonly ISymbolService _symbolService;
        private readonly ICacheManager _cacheManager;

        public StructureConverter(ISymbolService symbolService, ICacheManager cacheManager)
        {
            _symbolService = symbolService;
            _cacheManager = cacheManager;

            Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }

        public string Convert(ulong baseOfDll, string symbol)
        {
            var symbolType = _symbolService.GetSymbolType(baseOfDll, symbol);
            if (!symbolType.HasValue) return null;
            if (symbolType.Value.Tag != DbgHelpConstant.SymTagEnum.SymTagUdt)
            {
                Logger.WarnFormat("selected symbol {0} is not udt symbol", symbol);
                return null;
            }

            var childrenIndexes = _symbolService.GetSymbolTypeInfoChildren(baseOfDll, symbolType.Value.TypeIndex);
            if (childrenIndexes == null) return null;

            Logger.InfoFormat("found {0} children for symbol {1}", childrenIndexes.Length, symbol);

            var sb = new StringBuilder();
            sb.AppendFormat("[StructLayout(LayoutKind.Explicit, Size = 0x{0:x}, CharSet = CharSet.Auto, Pack = 1)]\r\n", symbolType.Value.Size)
                .AppendFormat("public struct {0}\r\n", symbol)
                .AppendLine("{");

            foreach (var childrenIndex in childrenIndexes)
            {
                var symbolInfo = _symbolService.GetSymbolTypeInfo(baseOfDll, childrenIndex);
                if (!symbolInfo.HasValue)
                {
                    Logger.WarnFormat("not found symbol for child index {0}", childrenIndex);
                    continue;
                }

                var name = _symbolService.GetSymbolTypeInfoName(baseOfDll, childrenIndex);
                var offset = _symbolService.GetSymbolTypeInfo<uint>(baseOfDll, childrenIndex, DbgHelpConstant.ImagehlpSymbolTypeInfo.TI_GET_OFFSET);

                var typeTag = (DbgHelpConstant.SymTagEnum) _symbolService.GetSymbolTypeInfo<uint>(baseOfDll, symbolInfo.Value.TypeIndex, DbgHelpConstant.ImagehlpSymbolTypeInfo.TI_GET_SYMTAG);

                switch (typeTag)
                {
                    case DbgHelpConstant.SymTagEnum.SymTagUdt:
                    {
                        var udtName = _symbolService.GetSymbolTypeInfoName(baseOfDll, symbolInfo.Value.TypeIndex);
                        WriteField(sb, offset, name, "byte", symbolInfo.Value.Size, string.Format("Udt {0}", udtName));
                        break;
                    }
                    case DbgHelpConstant.SymTagEnum.SymTagBaseType:
                    {
                        var baseType = (DbgHelpConstant.BasicType) _symbolService.GetSymbolTypeInfo<uint>(baseOfDll, symbolInfo.Value.TypeIndex, DbgHelpConstant.ImagehlpSymbolTypeInfo.TI_GET_BASETYPE);
                        var baseTypeLength = _symbolService.GetSymbolTypeInfo<ulong>(baseOfDll, symbolInfo.Value.TypeIndex, DbgHelpConstant.ImagehlpSymbolTypeInfo.TI_GET_LENGTH);
                        var typeName = FindTypeName(baseType, baseTypeLength);
                        WriteField(sb, offset, name, typeName, string.Format("{0} {1}", baseType, baseTypeLength));
                        break;
                    }
                    case DbgHelpConstant.SymTagEnum.SymTagPointerType:
                    {
                        WriteField(sb, offset, name, "IntPtr");
                        break;
                    }
                    case DbgHelpConstant.SymTagEnum.SymTagArrayType:
                    {
                        var size = _symbolService.GetSymbolTypeInfo<uint>(baseOfDll, symbolInfo.Value.TypeIndex, DbgHelpConstant.ImagehlpSymbolTypeInfo.TI_GET_LENGTH);
                        WriteField(sb, offset, name, "byte", size, "Array");
                        break;
                    }
                    case DbgHelpConstant.SymTagEnum.SymTagEnum:
                    {
                        var enumName = _symbolService.GetSymbolTypeInfoName(baseOfDll, symbolInfo.Value.TypeIndex);
                        var baseType = (DbgHelpConstant.BasicType) _symbolService.GetSymbolTypeInfo<uint>(baseOfDll, symbolInfo.Value.TypeIndex, DbgHelpConstant.ImagehlpSymbolTypeInfo.TI_GET_BASETYPE);
                        var baseTypeLength = _symbolService.GetSymbolTypeInfo<ulong>(baseOfDll, symbolInfo.Value.TypeIndex, DbgHelpConstant.ImagehlpSymbolTypeInfo.TI_GET_LENGTH);
                        var typeName = FindTypeName(baseType, baseTypeLength);
                        WriteField(sb, offset, name, typeName, string.Format("Enum {0}", enumName));
                        break;
                    }
                    default:
                    {
                        Logger.WarnFormat("can not handle tag type {0}", typeTag.ToString());
                        break;
                    }
                }
            }

            sb.Remove(sb.Length - 3, 2) // remove last line break '/r/n'
                .AppendLine("}");

            return sb.ToString();
        }

        private void WriteField(StringBuilder sb, uint offset, string name, string typeName, string comment = null)
        {
            sb.AppendFormat("\t[FieldOffset(0x{0:x})]\r\n", offset)
                .AppendFormat("\tpublic {0} {1};{2}\r\n", typeName, name, !string.IsNullOrEmpty(comment) ? string.Format(" // {0}", comment) : null)
                .AppendLine();
        }

        private void WriteField(StringBuilder sb, uint offset, string name, string typeName, uint arraySize, string comment = null)
        {
            sb.AppendFormat("\t[FieldOffset(0x{0:x})]\r\n", offset)
                .AppendFormat("\t[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x{0:x})]\r\n", arraySize)
                .AppendFormat("\tpublic {0}[] {1};{2}\r\n", typeName, name, !string.IsNullOrEmpty(comment) ? string.Format(" // {0}", comment) : null)
                .AppendLine();
        }

        private string FindTypeName(DbgHelpConstant.BasicType basicType, ulong length)
        {
            var types = _cacheManager.Get("StructureConverter.FindTypeName.Types", context => new[]
            {
                new {BasicType = DbgHelpConstant.BasicType.BtChar, Length = 1ul, Name = "sbyte"},
                new {BasicType = DbgHelpConstant.BasicType.BtLong, Length = 4ul, Name = "int"},
                new {BasicType = DbgHelpConstant.BasicType.BtULong, Length = 4ul, Name = "uint"},
                new {BasicType = DbgHelpConstant.BasicType.BtUInt, Length = 8ul, Name = "ulong"},
                new {BasicType = DbgHelpConstant.BasicType.BtUInt, Length = 2ul, Name = "ushort"},
                new {BasicType = DbgHelpConstant.BasicType.BtUInt, Length = 1ul, Name = "byte"},
                new {BasicType = DbgHelpConstant.BasicType.BtInt, Length = 2ul, Name = "short"},
                new {BasicType = DbgHelpConstant.BasicType.BtInt, Length = 4ul, Name = "int"},
                new {BasicType = DbgHelpConstant.BasicType.BtInt, Length = 8ul, Name = "long"},
            });

            var foundType = types.FirstOrDefault(x => x.BasicType == basicType && x.Length == length);
            if (foundType != null) return foundType.Name;

            Logger.WarnFormat("not found type for basic type {0}, length {1}", basicType, length);
            return basicType.ToString();
        }
    }
}